package com.rw.engine.ip.util;

import com.rw.tool.util.string.StringUtil;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * 判断IP是否合法的帮助类
 */
public class IpUtil {

    /**
     * 以通配符的方式验证ip是否符合模版
     *
     * @param template 传入的模版ip
     * @param ip       需要验证的ip
     * @return 是否符合
     */
    public static boolean verify(String ip, String template) {
        if (!isValidTemplate(template) || !isValid(ip)) {
            return false;
        }

        // 用.分割字符串 判断是否匹配
        String[] templateFragments = template.split("\\.");
        String[] ipFragments = ip.split("\\.");
        for (int i = 0; i < templateFragments.length; i++) {
            if (!"*".equals(templateFragments[i]) && !StringUtil.equals(templateFragments[i], ipFragments[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * 以正则方式验证ip是否符合模版
     *
     * @param ip    需要验证的ip
     * @param regex 正则规则
     * @return 是否符合
     */
    public static boolean match(String ip, String regex) {
        if (!isValid(ip)) {
            return false;
        }
        return ip.matches(regex);
    }

    /**
     * 判断ip是否合法
     * 这里有个烦人的 always inverted 警告
     * 这里直接忽略了
     */
    @SuppressWarnings("all")
    public static boolean isValid(String ip) {
        return isValidWithWildcard(ip, false);
    }

    /**
     * 判断ip模板是否合法
     */
    public static boolean isValidTemplate(String template) {
        return isValidWithWildcard(template, true);
    }

    /**
     * 判断IP字符串是否合法
     *
     * @param ip           ip字符串
     * @param withWildcard 是否允许IP字符串中携带通配符 '*'
     * @return 是否合法
     */
    private static boolean isValidWithWildcard(String ip, boolean withWildcard) {
        // IP字符串长度的最小值和最大值
        final int IP_LENGTH_MIN = 7;
        final int IP_LENGTH_MAX = 15;
        if (!StringUtil.lengthIn(ip, IP_LENGTH_MIN, IP_LENGTH_MAX)) {
            return false;
        }

        // IP字符串被 '.' 分割之后应有的段数
        final int FRAGMENTS_LENGTH = 4;

        String[] ipFragments = ip.split("\\.");
        if (ipFragments.length != FRAGMENTS_LENGTH) {
            return false;
        }

        // 判断每一段是否在0-255之间
        return Arrays.stream(ipFragments).allMatch(ipFragment -> isValidIpFragment(ipFragment, withWildcard));
    }

    /**
     * 判断IP片段是否合法。
     * 每一段IP片段应该在0-255之间
     *
     * @param ipFragment   ip片段
     * @param withWildcard 是否允许IP片段为统配符 '*'
     * @return 是否合法
     */
    private static boolean isValidIpFragment(String ipFragment, boolean withWildcard) {
        if (withWildcard && "*".equals(ipFragment)) {
            return true;
        }

        if (!StringUtil.isNumeric(ipFragment)) {
            return false;
        }

        int ipNum = Integer.parseInt(ipFragment);
        return ipNum >= 0 && ipNum <= 255;
    }

    /**
     * 通过HttpServletRequest获取IP地址。
     * 从多个协议头中尽可能地取出真实的IP地址。
     * <p>
     * 但正是因为我们尽可能的从多个协议头中取出IP，
     * 所以恶意请求可以设置某一个协议头，任意篡改IP。这是我们不想看到的，
     * 如果对获取的IP真实性有需求，那么请结合请求的转发情况自己编写对应的IP获取方法
     *
     * @param request http的请求对象
     * @return 获取到的IP
     */
    public static String getIp(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        // 有可能携带IP地址的协议头名称
        final List<String> IP_HEADER = Arrays.asList("Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP",
                "HTTP_X_FORWARDED_FOR", "X-Real-IP");
        // 若协议头x-forwarded-for中有IP地址
        String forwardIp = request.getHeader("x-forwarded-for");
        if (isIpNotNull(forwardIp)) {
            // 获取第一个非unknown的IP地址
            Optional<String> firstIp = Arrays.stream(forwardIp.split(",")).filter(IpUtil::isIpNotNull).findFirst();
            if (firstIp.isPresent()) {
                return firstIp.get();
            }
        }

        // 若其它协议头有IP地址
        for (String ipHeader : IP_HEADER) {
            String ip = request.getHeader(ipHeader);
            if (isIpNotNull(ip)) {
                return ip;
            }
        }

        // 若都没有，则直接返回RemoteAddr
        return request.getRemoteAddr();
    }

    /**
     * 从协议头X-Real-IP中获取IP
     * 如果获取失败了，则会调用request.getRemoteAddr()并返回
     *
     * 如果调用方设置协议头X-Real-IP，而刚好你的协议头又没有经过Nginx覆写
     * 那获取的IP地址就可以被任意篡改，请注意
     *
     * @param request http的请求对象
     * @return 获取到的IP
     */
    public static String getIpFromNginx(HttpServletRequest request) {
        String ip = request.getHeader("X-Real-IP");
        return isIpNotNull(ip) ? ip : request.getRemoteAddr();
    }

    /**
     * 判断ip是否有值。
     */
    private static boolean isIpNotNull(String ip) {
        return !StringUtil.isEmpty(ip) && !"unknown".equalsIgnoreCase(ip);
    }
}
